import * as d3 from 'd3'
export class ForceChart {
    divId = ''
    data = []
    chart = {}
    itemsName = []
    strength = -600
    distanceMin = 100
    distanceMax = 300
    degree = 1
    color = d3.scaleOrdinal(d3.schemeCategory20)
    simulation = d3.forceSimulation()

    constructor (divId) {
      this.divId = divId
    }

    draw () {
      this.initChart()
      this.initLayout()
      this.drawForce()
    }

    update () {
      this.updatePara()
      this.drawForce()
    }

    initChart () {
      this.chart.padding = {
        left: 0,
        right: 0,
        top: 0,
        bottom: 0
      }
      this.chart.divHeight = document.getElementById(this.divId).offsetHeight -
        this.chart.padding.top -
        this.chart.padding.bottom
      this.chart.divWidth = document.getElementById(this.divId).offsetWidth -
        this.chart.padding.left -
        this.chart.padding.right

      this.chart.svg = d3
        .select('#' + this.divId)
        .append('svg')
        .attr('width', this.chart.divWidth +
          this.chart.padding.left +
          this.chart.padding.right
        )
        .attr('height', this.chart.divHeight +
          this.chart.padding.top +
          this.chart.padding.bottom
        )
        .attr('class', 'svg_chart')
    }

    initLayout () {
      let chart = this.chart
      let forceLink = d3.forceLink().id(d => d.id).iterations(20)
      let forceCollide = d3.forceCollide().radius(d => 5).iterations(20)
      let forceCharge = d3.forceManyBody().strength(this.strength)
        .distanceMin(this.distanceMin)
        .distanceMax(this.distanceMax)
      let forceCenter = d3.forceCenter(this.chart.divWidth / 2, this.chart.divHeight / 2)

      this.simulation.velocityDecay(0.8)
        .force('link', forceLink)
        .force('collision', forceCollide)
        .force('charge', forceCharge)
        .force('center', forceCenter)

      this.chart.nodes = this.chart.svg.append('g')
        .attr('class', 'nodes')

      this.chart.links = this.chart.svg.append('g')
        .attr('class', 'links')

      this.ticked = function () {
        chart.nodes.selectAll('.node')
          .attr('cx', d => Math.max(5, Math.min(chart.divWidth - 5, d.x)))
          .attr('cy', d => Math.max(5, Math.min(chart.divHeight - 5, d.y)))

        chart.links.selectAll('.link')
          .attr('x1', d => Math.max(5, Math.min(chart.divWidth - 5, d.source.x)))
          .attr('y1', d => Math.max(5, Math.min(chart.divHeight - 5, d.source.y)))
          .attr('x2', d => Math.max(5, Math.min(chart.divWidth - 5, d.target.x)))
          .attr('y2', d => Math.max(5, Math.min(chart.divHeight - 5, d.target.y)))
      }
    }

    drawForce () {
      this.chart.linkedByIndex = {}
      let chart = this.chart
      let simulation = this.simulation
      let nodeDrag = d3.drag()
        .on('start', dragStart)
        .on('drag', dragged)
        .on('end', dragEnd)

      this.simulation
        .nodes(this.data.nodes)
        .on('tick', this.ticked)

      this.simulation
        .force('link')
        .links(this.data.links)

      let nodes = this.chart.nodes.selectAll('.node')
        .data(this.data.nodes)

      nodes
        .enter()
        .append('circle')
        .attr('class', 'node')
        .attr('cx', d => d.x)
        .attr('cy', d => d.y)
        .attr('r', d => 5)
        .style('fill', d => this.color(d.group))
        .on('mouseover', d => {
          NodeMouseOver(d, this.degree)
        })
        .on('mouseout', NodeMouseout)
        .call(nodeDrag)

      nodes.exit()
        .transition()
        .duration(750)
        .attr('r', 0)
        .remove()

      nodes.each(d => {
        d.isShow = 1
      })

      let links = this.chart.links.selectAll('.link')
        .data(this.data.links)

      links.enter()
        .append('line')
        .attr('class', 'link')
        .style('stroke', '#ccc')
        .style('stroke-width', 1.5)
        .style('stroke-opacity', 0.7)
        .each(d => {
          d.isShow = 0
          chart.linkedByIndex[d.source.index + ',' + d.target.index] = 1
        })

      links.exit()
        .transition()
        .duration(750)
        .attr('x2', d => Math.max(15, Math.min(chart.divWidth - 5, d.source.x)))
        .attr('y2', d => Math.max(15, Math.min(chart.divHeight - 5, d.source.y)))
        .remove()

      function dragStart (d) {
        if (!d3.event.active) {
          simulation.alphaTarget(0.3).restart()
        }
        d.fx = d.x
        d.fy = d.y
      }

      function dragged (d) {
        d.fx = d3.event.x
        d.fy = d3.event.y
      }

      function dragEnd (d) {
        if (!d3.event.active) {
          simulation.alphaTarget(0)
          // 设置fx,fy为null时不会将移动后的节点固定，反之，则固定
          // d.fx = null
          // d.fy = null
        }
      }

      function NodeMouseOver (d, degree) {
        filterData(d, degree)
        chart.nodes.selectAll('.node')
          .filter(d => d.isShow === 0)
          .style('opacity', 0.1)
        chart.links.selectAll('.link')
          .filter(d => d.isShow === 0)
          .style('stroke-opacity', 0.1)
      }

      function filterData (selectNode, degree) {
        let thisDegree = degree
        if (thisDegree <= 0) return 0
        chart.nodes.selectAll('.node').each(function (d) {
          let isConnect = isConnected(d, selectNode, chart.linkedByIndex)
          if (isConnect) {
            d.isShow = 1
            thisDegree -= 1
            filterData(d, thisDegree)
          }
        })

        chart.links.selectAll('.link').each(function (d) {
          if (d.source === selectNode || d.target === selectNode) {
            d.isShow = 1
          }
        })
      }

      function isConnected (a, b, linkedByIndex) {
        return linkedByIndex[a.index + ',' + b.index] || linkedByIndex[b.index + ',' + a.index] || a.index === b.index
      }

      function NodeMouseout (d) {
        chart.nodes.selectAll('.node')
          .style('opacity', 1)
          .each(d => {
            d.isShow = 0
          })
        chart.links.selectAll('.link')
          .style('stroke-opacity', 1)
          .each(d => {
            d.isShow = 0
          })
      }
    }

    updatePara () {
      let forceCharge = d3.forceManyBody().strength(this.strength)
        .distanceMin(this.distanceMin)
        .distanceMax(this.distanceMax)

      this.simulation.velocityDecay(0.8)
        .force('charge', forceCharge)

      this.simulation.alpha(0.6).alphaTarget(0).restart()
    }
}
